/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fjsei.yewu.report;

import com.alibaba.fastjson2.JSON;
import com.querydsl.core.BooleanBuilder;
import graphql.relay.Connection;
import graphql.schema.DataFetchingEnvironment;
import io.camunda.zeebe.spring.client.annotation.VariablesAsType;
import io.camunda.zeebe.spring.client.annotation.JobWorker;
import md.cm.base.Companies;
import md.cm.flow.ApprovalStm;
import md.cm.flow.ApprovalStmRepository;
import md.cm.geography.Village;
import md.cm.unit.Division;
import md.cm.unit.Unit;
import md.cm.unit.Units;
import md.log.Opinion_Enum;
import md.specialEqp.Eqp;
import md.specialEqp.QReport;
import md.specialEqp.Report;
import md.specialEqp.ReportRepository;
import md.specialEqp.inspect.*;
import md.specialEqp.type.Elevator;
import md.specialEqp.type.Vessel;
import md.system.User;
import org.fjsei.yewu.bpm.UniversalProcessVariables;
import org.fjsei.yewu.entity.fjtj.*;
import org.fjsei.yewu.exception.CommonGraphQLException;
import org.fjsei.yewu.graphql.DbPageConnection;
import org.fjsei.yewu.graphql.IdMapper;
import org.fjsei.yewu.input.IspCommonInput;
import org.fjsei.yewu.jpa.PageOffsetFirst;
import org.fjsei.yewu.pojo.jsn.ElevatorSvp;
import org.fjsei.yewu.pojo.sei.DeviceSnapshot;
import org.fjsei.yewu.resolver.Comngrql;
import org.fjsei.yewu.bpm.ApprovalStmService;
import org.fjsei.yewu.util.Tool;
import org.hibernate.Hibernate;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.validation.constraints.NotNull;
import java.util.*;
import java.util.stream.Collectors;

/**默认Unit模型的属性； graphQL接口;
 * 底下批处理方案还不是一种的。 注释掉的也是1个方案。
 * 每一个graphQL的Object对象type都需单独声明一个XxxController implements IdMapper<type> {} 否则Id都无法转换成Relay要的GlobalID的。
 * */

@Controller
public class ReportController extends Comngrql implements IdMapper<Report> {
	private final Units units;
	private final Companies companies;
	private final TaskRepository tasks;
	private final IspRepository isps;
	//private final ApprovalStmRepository  approvalStms; 基类已经注入该表了，就不要重复：两个实例。
	private final ReportRepository  reports;
	private final ApprovalStmService approvalStmService;
	/** registry参数：IDEA报红却能运行成功，实际运行注入DefaultBatchLoaderRegistry;
     *  消除报红，测试平台Units没有报红 https://blog.csdn.net/zy103118/article/details/84523452
	 * 添加了 @ComponentScan({"md","org.fjsei.yewu"}) 以后: Units报红消除了。
	 * */
	public ReportController(BatchLoaderRegistry registry, Units units, Companies companies, TaskRepository tasks, IspRepository isps, ReportRepository reports, ApprovalStmService approvalStmService) {
		this.units = units;
		this.companies = companies;
		this.tasks = tasks;
		this.isps = isps;
		this.reports = reports;
		this.approvalStmService = approvalStmService;
	}
	//实际可直接node(GlobalID)替换的；   不能用Long id，graphQL已改成ID
	//参数不对Cannot invoke "Object.getClass()" because "source" is null， 需要@Argument()改名，或者Report report无法注入的参数。
	@QueryMapping
	public Report getReport(@Argument("id") String sId) {
		Report report = entityOf(sId,Report.class);
//		Tool.ResolvedGlobalId gId= Tool.fromGlobalId(sId);
//		Long id=Long.valueOf(gId.getId());
		//Report report = reportRepository.findById(id).orElse(null);
		//是否需要重新初始化技术参数设备基本字段呢？
		return report;
	}
	@MutationMapping
	@Transactional
	public Report modifyOriginalRecordData(@Argument String id,@Argument Integer operationType,@Argument String data,@Argument String deduction) {
		User user=checkAuth();
		if(null==user)  return null;
		//if(!emSei.isJoinedToTransaction())      emSei.joinTransaction();
		Report orRep= entityOf(id,Report.class);
		//Report originalRecord= reports.findById(id).orElse(null);
		if(orRep == null)     throw new CommonGraphQLException("没有该原始记录", id);
		ApprovalStm stm=orRep.getStm();
		Assert.isTrue(Procedure_Enum.MAKE.equals(stm.getSta()) || Procedure_Enum.BEGIN.equals(stm.getSta()),"无效状态");
		boolean matchusr= stm.getAuthr().stream().anyMatch(man -> {
			return man.equals(user);
		});
		Assert.isTrue(matchusr || user.equals(stm.getMaster()),"检验员校核人才能改");
		if(1==operationType)
			orRep.setData(data);
		else if(2==operationType){
			//权限验证
		}
		reports.save(orRep);
		return orRep;
	}
	/**报告编制结束，进入zeebe流程: 开始多人签字
     * 流转 审批过程： 针对出报告的。
	 * 状态步骤环节预定义好的，外部逻辑控制流程，流转历史回撤按时间和环节标记来追溯后退，光靠ApprovalStm无法支持流程控制。
	 * 尽量抽象？ 做成通用的接口。 授权也不一样啊！ ？ 用户权限控制还得关联实际接口  没法整。
	 * 已经走过流程实例的情况：修改报告后重新流转的请使用flowTo()接口。
	 * */
	@MutationMapping
	@Transactional
	public String  flowReport(@Argument String repId,@Argument String uri,@Argument Opinion_Enum allow,@Argument String memo,@Argument Integer days) {
		User user=checkAuth();
		if(null==user)  return "没登录";
		//Report report = reportRepository.findById(repId).orElse(null);
		Report report = entityOf(repId,Report.class);
		if(null==report)    return "没找到报告";
		//ApprovalStm可以保存历史配置，不是在本命令接口提供所有的预设的参数。
		ApprovalStm stm=report.getStm();	  //仅仅是状态和历史操作记录，不是真正的流程引擎服务。
		if(null==stm)   return "没状态机";
		Assert.isTrue(user.equals(stm.getMaster()),"校核人才能开启流程");
		Assert.isTrue(null==stm.getPrId(),"已有流程");
		Assert.isTrue(Procedure_Enum.BEGIN.equals(stm.getSta()) || Procedure_Enum.MAKE.equals(stm.getSta()),"已流转");
		Boolean subitem=true;
		if(report.getIsp().getReport()==report)		subitem=false;
		//流转时刻 每个阶段都要单独的验证 :人工审查+ 自动校对+触发业务功能。
//		Set<User> users=new HashSet<>();
//		userIds.forEach(item -> {	 	//todo:剔除校核人自己
//			User man = entityOf(item,User.class);
//			//User man=userRepository.findById(item).orElse(null);
//			Assert.isTrue(man != null,"未找到user:"+item);
//			users.add(man);
//		});		if(users.size()<=0)  return "授权用户为空";	  //只有一人还是校核人的情况？ 待签名人?空集
		Eqp eqp= report.getIsp().getDev();
		String ident;
		if(eqp==null) 	ident= report.getIsp().getBus().getIdent();
		else 	ident= StringUtils.hasText(eqp.getCod())? eqp.getCod(): eqp.getOid();
		String topic= ident+"报告"+report.getIsp().getNo();

		String processId= approvalStmService.startFlow("ReportApproval",topic,repId, uri, stm, allow, memo, days, stm.getAuthr(), subitem);
		if(null==processId)		//关键的Process Instance Id，@@要避免遗忘掉已经生成的流程！zeebe引擎存储的遗漏占用;
			return "移交失败";
		return  processId;
	}
	/**处理zeebe服务的UserTask列表， 完成后当前用户 确认审批或者回退等操作或签字过关。
	 * 修改报告后重新再一次结束编制状态这时刻校核人员有权再次重新流转的也用这个接口,而不是全新未发生流程行为的flowReport()。
	 * */
	@MutationMapping
	@Transactional
	public String  flowTo(@Argument String entId, @Argument String userTaskId, @Argument Opinion_Enum allow, @Argument String memo,
						  @Argument List<String> mens, @Argument Integer days, @Argument String uri) {
		//todo: 不一定都得是Report实体类。 申请单都行;
		Report report = entityOf(entId,Report.class);   //或者其他的申请单实体 都 instance of FlowStat
		if(null==report)    return "没找到报告";
		//??直接从zeebe服务提取 流程变量：统一流程变量ent 代表挂接的申请单或报告的实体表ID: GlobalID;
		//应当把关联的Report报告id给我传递进来啊，写入流程日志实体表也得用它。普通申请单的实体模型对象也有ID。
		String rUserTaskId= approvalStmService.flowTo(userTaskId, report, allow, memo,days, null,uri);
		if(null==rUserTaskId)	return "移交失败";
		return  rUserTaskId;
	}
	/**当前我的待处理的分项报告(不含正常主报告同时有涉及的情况)：
	 * 分项报告：正常应该扣除了我已经在该Isp的主报告当中的情况，若分项没有终结主报告肯定也没有终结的。
	 * 用QueryDSL做超复杂的关联关系的过滤多条件查询，的确比较容易看得懂，比起传统的人工书写SQL语句真得是强了太多！！传统编码很容易搞错逻辑。
	 * */
	@QueryMapping
	public Connection<Report> findMeSubReports(@Argument String orderBy, @Argument Boolean asc, @Argument IspCommonInput where, @Argument Integer first, @Argument String after, @Argument Integer last, @Argument String before, DataFetchingEnvironment env) {
		User user= checkAuth();
		if(user==null)   return null;
		DbPageConnection<Report> connection=new DbPageConnection(env);
		int offset=connection.getOffset();
		int limit=connection.getLimit();
		Pageable pageable;
		if (!StringUtils.hasLength(orderBy))
			pageable = PageOffsetFirst.of(offset, limit);
		else
			pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
		QReport qm = QReport.report;
		BooleanBuilder builder = new BooleanBuilder();
		BooleanBuilder subRepPr = new BooleanBuilder();
		subRepPr.or(qm.isp.report.ne(qm));
		BooleanBuilder userPr = new BooleanBuilder();
		//默认：在这个位置qm.isp.report.stm就会出现null 空指针(IDEA观察到)，但可能也根本就没提示的  直接歇菜。querydsl解决办法@QueryInit("report.stm.master")多对一一对一的字段才会出现此类问题。
		userPr.or(qm.stm.master.eq(user)).or(qm.stm.authr.contains(user));
		BooleanBuilder staPr = new BooleanBuilder();
		staPr.or(qm.stm.sta.eq(Procedure_Enum.BEGIN)).or(qm.stm.sta.eq(Procedure_Enum.MAKE));
		//需要排除我已经是关联Isp的主报告当中的人员了。
		BooleanBuilder meInMainRepPr = new BooleanBuilder();
		//嵌套多层可能前端发现Cannot read field \"master\" because \"qm.isp.report.stm\" is null 后端实际出错：QueryDSL对3层以上一对一多对一字段没特别注解是空指针了。
		//【问题】多个@QueryInit会相互影响啊？ ?抵消，@QueryInit("report.stm.master")要重复测试 @QueryInit({"e3.*", "e33.e4", "e333"}), @QueryInit("e3.e4"), * , *.*;
		meInMainRepPr.or(qm.isp.report.stm.master.eq(user)).or(qm.isp.report.stm.authr.contains(user));
		//类似于SQL语句的Where的多个逻辑汇总 AND 4个条件;  排除面积大的条件放在前部率先执行。
		builder.and(userPr).and(subRepPr).and(staPr).and(meInMainRepPr.not());
		if(StringUtils.hasText(where.getServu())) {
			Unit unit=fromInputUnitGlobalID(where.getServu());
			Assert.notNull(unit,"未找到Unit:"+where.getServu());
			//不考虑关联isp.servu.搜寻，依据isp.bus.task.servu做关联，task表数据量更少，冷数据已清理掉了。
			//【QueryDsl】错Cannot read field task because qm.isp.bus is null，初始化加到报错的上两层关联Report模型当中做的; bus->Isp->Report;
			builder.and(qm.isp.bus.task.servu.id.eq(unit.getId()));
		}
		if(StringUtils.hasText(where.getCod()))
			builder.and(qm.isp.dev.cod.containsIgnoreCase(where.getCod()));
		if(StringUtils.hasText(where.getOid()))
			builder.and(qm.isp.dev.oid.containsIgnoreCase(where.getOid()));
		if(StringUtils.hasText(where.getIdent()))
			builder.and(qm.isp.bus.ident.containsIgnoreCase(where.getIdent()));
		if(StringUtils.hasText(where.getNo()))
			builder.and(qm.isp.no.containsIgnoreCase(where.getNo()));
		//【神奇了 querydsl】实际上这builder在这直接照抄findMeIsps(){}里面的竟然也可以！ 会从QIsp QDetail自动生成关联关系的。我都没定义QDetail的必要了。
		//这里直接在IDEA调试环节，查看 builder 变量的描述 就能判定代码是否正确。
		Slice<Report> rpage= (Slice<Report>)reports.findAll(builder,pageable);
		List<Report> isps=(List<Report>) rpage.toList();
		return connection.setListData(isps).get(env);
	}
	/**加个报告；分项的。 主报告 本身自带具备排他性。
	 * Isp底下挂接的 分项报告同一个modeltype类型的可以允许多个，主报告modeltype只能唯一个。
	 * 分项报告和收费项目没有必然联系；派工环节Task任务负责人自己决定是否增加哪些分项报告。
	 *  任务负责人dispatchTaskTo给校核人检验人员审核人审批人。默认的分项采取和主报告(dispatchTaskTo:TASK初始化时就有生成的主报告)主报告相一致的人员配备的。
	 *  后续的，任务负责人可以增加分项报告同时配置人员参数。
	 *  任务责任人dispatchTaskTo(taskDate: Date）这个操作应当是距离真正的检验工作日期非常接近的日子去做。
	 *  任务责任人才有权利执行newReport｛主报告或分项报告以及人员配置甚至收费项目参数设置的｝
	 */
	@MutationMapping
	@Transactional
	public Report newReport(@Argument("isp") String ispId,@Argument("master") String masterId,
							@Argument("ispMens") List<String> ispmenIds,@Argument("reviewer") String reviewerId,
							@Argument("approver") String approverId,@Argument String modeltype,@Argument Short modelversion) {
		User user=checkAuth();
		if(null==user)  return null;
//		Detail detail= entityOf(detId,Detail.class);
//		Assert.notNull(detail,"未找到Detail:"+detId);
		Isp isp =entityOf(ispId,Isp.class);
		Assert.notNull(isp,"未找到Isp");
		User verify = entityOf(masterId,User.class);
		Assert.notNull(verify,"未找到user:"+masterId);
		List<User> ispMens=  new ArrayList<>();
		ispmenIds.stream().forEach(item -> {
			User aUser = entityOf(item,User.class);
			Assert.isTrue(aUser != null,"未找到user:"+item);
			ispMens.add(aUser);
		});
		User reviewer = entityOf(reviewerId,User.class);
		Assert.isTrue(reviewer != null,"未找到user:"+reviewerId);
		User approver = entityOf(approverId,User.class);
		Assert.isTrue(approver != null,"未找到user:"+approverId);
		//报告编号码不是这里设置的， 应该在Detail/Isp设备加到Task的时间，就是分配Task给负责人 dispatchToLiabler 操作就定义好的。后面分项报告增加删除也要保持no不变。
		//万一删除了主报告忘记了旧的no编码？ 可以从分配Task给负责人环节再次补回新的no。
		String repNo= isp.getNo();		//"JD2020FTC00004";
		Report report = new Report(ReportType_Enum.WEB,repNo,isp);
		report.setModeltype(modeltype);
		report.setModelversion(modelversion);
		java.util.Date  now= new java.util.Date();
		//report.setUpLoadDate(new java.sql.Date(now.getTime()));
		ApprovalStm approvalStm=new ApprovalStm();
		approvalStm.setMaster(verify);		//负责启动zeebe流转
		approvalStm.setAuthr(ispMens);		//要签字的多个检验员
		approvalStm.setReviewer(reviewer);
		approvalStm.setApprover(approver);
		report.setStm(approvalStm);
		approvalStms.save(approvalStm);
		report.setUpLoadDate(now);
		reports.save(report);
		//若主报告=null的，默认设为率先设置为主报告，优先配置主报告，需要修改主报告的可以先删除主报告后再添加一个新报告，否则都是分项报告，主报告唯一性。
		if(null==isp.getReport() )
			isp.setReport(report);		 //默认还没有的 算主报告
		isps.save(isp);
		return report;
	}
	/**前端看到的都是主报告snapshot， 为避免混淆，后端尽量不要用！
	 * 分项报告采用和主报告同一份 snapshot 存储，若是分项报告则数据库该字段实际=null; 主报告才需要设置数据库snapshot字段。
	 * */
	@SchemaMapping(field="snapshot")
	public String  snapshot(Report report, DataFetchingEnvironment env) {
		Isp isp = report.getIsp();
		return  (null==isp.getReport()? null: isp.getReport().getSnapshot() );
	}
}

